#include "omp.h"
#include "cuda.h"
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>

// Constantes
#define NUM_CHANNELS    4
#define BLOCK_SIZE      5
#define BLOCK_SQUARE    25
#define BLOCK_OFFSET    2
#define BLOCK_PADDING   4

// Threads
#define NUM_THREADS     128

// DEBUG
#define DEBUG           1

// Estrutura de dados
typedef unsigned char PixelChannel;
typedef short int ImageDimension;
typedef struct image {
    PixelChannel *data;
    ImageDimension width, height;
} Image;

int getLinearPosition(ImageDimension i, ImageDimension j, ImageDimension k, Image *in) {
    return (i * NUM_CHANNELS + j * NUM_CHANNELS * in->height + k);
}

Image* createImage(ImageDimension width, ImageDimension height, int channels) {
    // Imagem
    Image* image;

    // Alocacao da imagem
    image = (Image*) malloc(sizeof(Image));

    // Dados
    image->width = width;
    image->height = height;

    // Alocacao dos pixels da imagem
    image->data = (PixelChannel*) malloc(sizeof(PixelChannel) * width * height * channels);

    // Retorno
    return image;
}

void deleteImage(Image *image) {
    // Desalocacao
    free(image->data);
    free(image);
}

void readImage(FILE* inputFile, Image *image) {
    // Iteradores
    int i, j; // Pixels
    int k; // Canais

    // Leitura da imagem
    for(i = BLOCK_OFFSET; i < image->width - BLOCK_OFFSET; i++)
        for(j = BLOCK_OFFSET; j < image->height - BLOCK_OFFSET; j++)
            for(k = 0; k < NUM_CHANNELS; k++)
                fread(&image->data[getLinearPosition(i, j, k, image)], sizeof(PixelChannel), 1, inputFile);
}

void writeImage(FILE* outputFile, Image *image) {
    // Iteradores
    int i, j; // Pixels
    int k; // Canais

    // Armazenamento da imagem
    for(i = BLOCK_OFFSET; i < image->width - BLOCK_OFFSET; i++)
        for(j = BLOCK_OFFSET; j < image->height - BLOCK_OFFSET; j++)
            for(k = 0; k < NUM_CHANNELS; k++)
                fwrite(&image->data[getLinearPosition(i, j, k, image)], sizeof(PixelChannel), 1, outputFile);
}

void printImage(Image *image) {
    // Iteradores
    int i, j; // Pixels
    int k; // Canais

    for(k = 0; k < NUM_CHANNELS; k++) {
        printf("Channel %d\n", k);
        for(i = BLOCK_OFFSET; i < image->width - BLOCK_OFFSET; i++) {
            for(j = BLOCK_OFFSET; j < image->height - BLOCK_OFFSET; j++) {
                printf("%3d ", image->data[getLinearPosition(i, j, k, image)]);
            }
            printf("\n");
        }
        printf("\n");
    }
}

__device__ int getLinearPositionCuda(ImageDimension i, ImageDimension j, ImageDimension k, Image *in) {
    return (i * NUM_CHANNELS + j * NUM_CHANNELS * in->height + k);
}

__global__ void applyFilter(Image *inputImage, PixelChannel *inputData, PixelChannel *outputData, int blocksPerChannel, int blockSizeX, int blockSizeY) {
    // Iteradores
    int i, j;
    // Variaveis
    int sum;
    int id, blockId, chId;
    int initialX, initialY, finalX, finalY;

    id = threadIdx.x;
    blockId = id / blocksPerChannel;
    chId = id % blocksPerChannel;

    // Posicionamento em pixels das fronteiras do bloco
    initialX = (blockId / 2) * blockSizeX + BLOCK_OFFSET;
    initialY = (blockId % 2) * blockSizeY + BLOCK_OFFSET;
    finalX = initialX + blockSizeX;
    finalY = initialY + blockSizeY;

    // Media dos vizinhos
    for(i = initialX; i < finalX; i++)
        for(j = initialY; j < finalY; j++) {
            // Soma dos pixels
            sum =
                inputData[getLinearPositionCuda(i - 2, j - 2, chId, inputImage)] +
                inputData[getLinearPositionCuda(i - 2, j - 1, chId, inputImage)] +
                inputData[getLinearPositionCuda(i - 2, j    , chId, inputImage)] +
                inputData[getLinearPositionCuda(i - 2, j + 1, chId, inputImage)] +
                inputData[getLinearPositionCuda(i - 2, j + 2, chId, inputImage)] +

                inputData[getLinearPositionCuda(i - 1, j - 2, chId, inputImage)] +
                inputData[getLinearPositionCuda(i - 1, j - 1, chId, inputImage)] +
                inputData[getLinearPositionCuda(i - 1, j    , chId, inputImage)] +
                inputData[getLinearPositionCuda(i - 1, j + 1, chId, inputImage)] +
                inputData[getLinearPositionCuda(i - 1, j + 2, chId, inputImage)] +

                inputData[getLinearPositionCuda(i    , j - 2, chId, inputImage)] +
                inputData[getLinearPositionCuda(i    , j - 1, chId, inputImage)] +
                inputData[getLinearPositionCuda(i    , j    , chId, inputImage)] +
                inputData[getLinearPositionCuda(i    , j + 1, chId, inputImage)] +
                inputData[getLinearPositionCuda(i    , j + 2, chId, inputImage)] +

                inputData[getLinearPositionCuda(i + 1, j - 2, chId, inputImage)] +
                inputData[getLinearPositionCuda(i + 1, j - 1, chId, inputImage)] +
                inputData[getLinearPositionCuda(i + 1, j    , chId, inputImage)] +
                inputData[getLinearPositionCuda(i + 1, j + 1, chId, inputImage)] +
                inputData[getLinearPositionCuda(i + 1, j + 2, chId, inputImage)] +

                inputData[getLinearPositionCuda(i + 2, j - 2, chId, inputImage)] +
                inputData[getLinearPositionCuda(i + 2, j - 1, chId, inputImage)] +
                inputData[getLinearPositionCuda(i + 2, j    , chId, inputImage)] +
                inputData[getLinearPositionCuda(i + 2, j + 1, chId, inputImage)] +
                inputData[getLinearPositionCuda(i + 2, j + 2, chId, inputImage)] ;

            // Media aritmetica
            outputData[getLinearPositionCuda(i, j, chId, inputImage)] = sum / BLOCK_SQUARE;
        }
}

// Rotina principal
int main(int argc, char* argv[]) {
    // VARIAVEIS
    Image *inputImage, *outputImage;
    ImageDimension width, height;
    FILE *inputFile, *outputFile;
    double startTime, endTime;

    // VARIAVEIS CUDA
    int size;
    PixelChannel *devDataIn, *devDataOut;
    int blocksPerChannel, blockSizeX, blockSizeY;

    // LEITURA DOS DADOS
    inputFile = fopen("data/image.in", "rb");
    if(inputFile == NULL) {
        printf("ERRO! Arquivo invalido!\n");
        return -1;
    }

    // Dimensoes da imagem
    fread(&width, sizeof(ImageDimension), 1, inputFile);
    fread(&height, sizeof(ImageDimension), 1, inputFile);
    size = width * height * NUM_CHANNELS;

    // ALOCACAO CUDA
    cudaMalloc((void**) &devDataIn, size * sizeof(PixelChannel));
    cudaMalloc((void**) &devDataOut, size * sizeof(PixelChannel));

    // Imagem de entrada
    inputImage = createImage(width + BLOCK_PADDING, height + BLOCK_PADDING, NUM_CHANNELS);
    readImage(inputFile, inputImage);
    fclose(inputFile);

    // Imagem de entrada: CUDA
    cudaMemcpy(devDataIn, inputImage->data, size * sizeof(PixelChannel), cudaMemcpyHostToDevice);

    // DEBUG: Imprime a imagem de entrada
    if(DEBUG) {
        printf("INPUT IMAGE\n");
        printImage(inputImage);
    }

    // Imagem de saida
    outputImage = createImage(width + BLOCK_PADDING, height + BLOCK_PADDING, NUM_CHANNELS);

    // Aplica o filtro
    blocksPerChannel = NUM_THREADS / NUM_CHANNELS;
    blockSizeX = (inputImage->width - BLOCK_PADDING) / (blocksPerChannel / 2);
    blockSizeY = (inputImage->height - BLOCK_PADDING) / (blocksPerChannel / 2);
    applyFilter<<<1, NUM_THREADS>>>(inputImage, devDataIn, devDataOut, blocksPerChannel, blockSizeX, blockSizeY);

    // Imagem de saida: CUDA
    PixelChannel *hostDataOut;
    hostDataOut = (PixelChannel*) malloc(size * sizeof(PixelChannel));
    cudaMemcpy(hostDataOut, devDataOut, size * sizeof(PixelChannel), cudaMemcpyDeviceToHost);

    outputImage->data = hostDataOut;


    // DEBUG: Imprime a imagem de saida
    if(DEBUG) {
        printf("OUTPUT IMAGE\n");
        printImage(outputImage);
    }

    // ARMAZENAMENTO DOS RESULTADOS
    outputFile = fopen("data/image.out", "wb");
    writeImage(outputFile, outputImage);
    fclose(outputFile);

    // DESALOCACAO
    deleteImage(inputImage);
    deleteImage(outputImage);

    // Tempo final
    //endTime = omp_get_wtime();

    // Tempo de execucao
    //printf("time: %f\n", endTime - startTime);

    // Retorno
    return 0;
}
